24.2 Exception Handling: The Short Story
Compared to some other core language topics we've
met, exceptions are a fairly light-weight tool in Python. Because
they are so simple, let's jump right into an initial
example. Suppose you've coded the following
function:
>>> def fetcher(obj, index):
... return obj[index]
There's not much to this function—it simply
indexes an object on a passed-in index. In normal operation, it
returns the result of legal indexes:
>>> x = 'spam'
>>> fetcher(x, 3) # Like x[3]
'm'
However, if you ask this function to index off the end of your
string, you will trigger an exception when your function tries to run
obj[index]. Python detects out-of-bounds sequence
indexing, and reports it by raising (triggering)
the built-in
IndexError exception:
>>> fetcher(x, 4)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "<stdin>", line 2, in fetcher
IndexError: string index out of range
Technically, because this exception is not caught by your code, it
reaches the top level of the program and invokes the
default exception
handler—which simply prints the standard error
message. By this point in the book, you've probably
seen your share of standard error messages. They include the
exception that was raised, along with a stack
trace—a list of the lines and functions active when
the exception occurred. When coding interactively, the file is just
"stdin" (standard input stream) or
"pyshell" (in IDLE), so file line
numbers are not very meaningful here.
In a more realistic program launched outside the interactive prompt,
the default handler at the top also terminates
the
program immediately. That course of action makes sense for simple
scripts; errors often should be fatal, and the best you can do is
inspect the standard error message. Sometimes this
isn't what you want, though. Server programs, for
instance, typically need to remain active even after internal errors.
If you don't want the default exception behavior,
wrap the call in a try statement to catch the
exception yourself:
>>> try:
... fetcher(x, 4)
... except IndexError:
... print 'got exception'
...
got exception
>>>
Now, Python jumps to your handler (the block
under the except clause that names the exception
raised) automatically when the exception is triggered while the
try block runs. When working interactively like
this, after the except clause runs we wind up back
at the Python prompt. In a more realistic program,
try statements not only catch
exceptions, but also recover from them:
>>> def catcher( ):
... try:
... fetcher(x, 4)
... except IndexError:
... print 'got exception'
... print 'continuing'
...
>>> catcher( )
got exception
continuing
>>>
This time, after the exception is caught and handled, the program
resumes execution after the entire try statement
that caught it—which is why we get the
"continuing" message here. You
won't see the standard error message, and your
program will continue on its way normally.
Exceptions can be raised both by Python and you, and can be caught or
not. To trigger an exception manually, simply run a
raise (or assert) statement;
user-defined exceptions are caught the same way as built-ins:
>>> bad = 'bad'
>>> try:
... raise bad
... except bad:
... print 'got bad'
...
got bad
If not caught, user-defined exceptions reach the top-level default
exception handler, and terminate your program with a standard error
message. In this case, the standard message includes the text of the
string used to identify the exception:
>>> raise bad
Traceback (most recent call last):
File "<pyshell#18>", line 1, in ?
raise bad
bad
In other cases, the error message may include text provided by
classes used to identify exceptions. As
we'll see in the next chapter, class-based
exceptions allow scripts to build exception categories (and other
things):
>>> class Bad: pass
...
>>> def doomed( ): raise Bad( )
...
>>> try:
... doomed( )
... except Bad:
... print 'got Bad'
...
got Bad
>>>
Finally, try statements can also say
finally: the
try/finally combination
specifies termination actions that always execute
"on the way out"—whether an
exception happens in the try block or not:
>>> try:
... fetcher(x, 3)
... finally:
... print 'after fetch'
...
'm'
after fetch
Here, when the try block finishes without an
exception, the finally block runs, and then the
program resumes after the entire try. In this
case, this statement seems a bit silly—we might as well have
simply typed the print right after a call to the
function, and skipped try altogether:
fetcher(x, 3)
print 'after fetch'
There is a problem with coding this way, though: if the function call
raises an exception, we will never reach the
print. The
try/finally lets us do
better—when an exception does occur in the
try block, finally blocks are
executed while the program is being unwound:
>>> def after( ):
... try:
... fetcher(x, 4)
... finally:
... print 'after fetch'
... print 'after try?'
...
>>> after( )
after fetch
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "<stdin>", line 3, in after
File "<stdin>", line 2, in fetcher
IndexError: string index out of range
Here, we didn't get the "after
try?" message, because control does not resume after
the try/finally when an
exception occurs. Instead, Python jumps back to run the
finally action, but then keeps
propagating the exception to a prior handler (in
this case, to the default handler at the top). If you change the call
inside this function so as not to trigger an exception, the
finally code still runs, but the program continues
after the try:
>>> def after( ):
... try:
... fetcher(x, 3)
... finally:
... print 'after fetch'
... print 'after try?'
...
>>> after( )
after fetch
after try?
>>>
In practice, try/except
combinations are useful for catching and recovering from exceptions,
and try/finally comes in handy
to guarantee that termination actions will fire regardless of the
exceptions that may occur in the try
block's code. For instance, you might use
try/except to catch errors
raised by code that you import from a third-party library, and
try/finally to ensure that
calls to close files or terminate server connections are always run.
We'll see some such practical examples later in this
part of the book.
That is a majority of the exception story; it really is a simple
tool. In the rest of this part, we'll fill in some
of the details of the statements involved, show you the other sorts
of clauses that can appear under a try, and
discuss string and class-based exception objects.
Python exceptions are a high-level control flow device. They may be
raised either by Python or by your own programs; in both cases, they
may be ignored (to trigger the default error message), or may be
caught by try statements (to be processed by your
code). Python's raise and
assert statements trigger exceptions on demand.
The try statement comes in two formats—one
that handles exceptions and one that executes finalization code
whether exceptions occur or not. Let's take a
deeper look at these statements' general forms.
|